CSRF 공격 방지와 csurf middleware

✒️ 2025-05-26 14:11 내용 수정

Node.js 교과서 개정 3판 내용 정리


CSRF(Cross Site Request Forgery)

사용자가 의도치 않게 공격자가 의도한 행동을 하게 만드는 공격


// express server
app.use(session({
  secret: 'custom-cookie-secret',
  cookie: {
    sameSite: 'Strict' // 또는 'Lax'
  }
}));

2. CORS 설정

// express server
const cors = require('cors');

const corsOptions = {
  origin: 'https://your-allowed-origin.com'
};

app.use(cors(corsOptions));

csurf middleware

CSRF 공격을 막기 위한 패키지

npm install csurf

참고 자료 : reddit Should I deploy CSRF token for react SPA?, reddit How do you protect against CSRF attacks in a react app?, stackoverflow CSRF Token necessary when using Stateless(=Sessionless) Authentication?


1. 기본 예시

// server.js
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const express = require('express');
const app = express();

const csrfProtection = csrf({ cookie: true }); // cookie-parser 필요
const parseFomr = bodyParser.urlencoded({ extended : false });

// cookie-parser 사용
app.use(cookieParser());

// 미들웨어 형식의 동작
// /test라는 라우터로 접근하면 토큰 발행
app.get('/test', csrfProtection, (req, res) => { 
  res.render('csrf', { csrfToken: req.csrfToken() });
});

// form에서 보낸 데이터를 처리하는 라우터
// 프론트에서 렌더링된 CSRF 토큰을 form 제출 시에 같이 제출해야 함
app.post('/test', csrfProtection, (req, res) => {
  res.send('csrf-ok');
});
<form action="/process" method="POST">
	<input type="hidden" name="_csrf" value="{{csrfToken}}">
	<button type="submit">Submit</button>
</form>

2. AJAX 사용

<meta name="csrf-token" content="{{csrfToken}}">
// meta 태그에 저장된 token을 가져오기
var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
 
// 요청 생성
fetch('/process', {
  credentials: 'same-origin', // 요청에 cookie 추가
  headers: {
    'CSRF-Token': token // csrf token을 header에 추가
  },
  method: 'POST',
  body: {
	name : 'test'
  }
});

3. React에서 Token을 받고 전송하기

// server.js
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

// token 발행 요청
app.get('/csrf', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

const bodyParser = require('body-parser'); // form 데이터 파싱 설정
const parseForm = bodyParser.urlencoded({ extended: false });

// POST, PUT, DELETE 요청에 대해 csrfToken 검증
app.post('*', parseForm, csrfProtection, (req, res, next) => { 
  next();
});
app.put('*', parseForm, csrfProtection, (req, res, next) => {
  next();
});
app.delete('*', parseForm, csrfProtection, (req, res, next) => {
  next();
});
// App.js
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';

function App() {
	const navigate = useNavigate();
	
	useEffect(()=>{
		axios.get('/csrf')
		  .then(res=>{
			// axios header를 설정하여 이후 모든 종류의 요청의 header에 csrfToken이 포함되도록 한다
			axios.defaults.headers.common['X-CSRF-Token'] = res.data.csrfToken;
		  })
		  .catch(err=>{
			navigate('/error/400'); // 에러가 생기면 에러 페이지로 보낸다
		  });
	}, [navigate]);
	
	return(</>)
}

csrf 1.png

csrf 2.png

csrf 3.png

// App.js
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';

function App() {
	const navigate = useNavigate();
	
	useEffect(()=>{
		axios.get('/csrf')
		  .then(res=>{
			// 검증 확인을 위해 Header에 들어갈 csrfToken 값을 임의로 수정했다.
			axios.defaults.headers.common['X-CSRF-Token'] = res.data.csrfToken + 'sld';
		  })
		  .catch(err=>{
			navigate('/error/400');
		  });
	}, [navigate]);
	
	return(</>)
}

csrf 4.png

csrf 5.png
csrf 6.png